<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
        var startTagClose = /^\s*(\/?)>/;
        var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
        var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

        var no = function (a, b, c) { return false; };
        var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
        var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
        var startTagOpen = new RegExp(("^<" + qnameCapture));

        var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
        var isPlainTextElement = makeMap('script,style,textarea', true);
        var isIgnoreNewlineTag = makeMap('pre,textarea', true);
        var shouldIgnoreFirstNewline = function (tag, html) { return tag && isIgnoreNewlineTag(tag) && html[0] === '\n'; };

        var decodingMap = {
            '&lt;': '<',
            '&gt;': '>',
            '&quot;': '"',
            '&amp;': '&',
            '&#10;': '\n',
            '&#9;': '\t',
            '&#39;': "'"
        };
        var encodedAttr = /&(?:lt|gt|quot|amp|#39);/g;
        var encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g;

        // #5992
        var isIgnoreNewlineTag = makeMap('pre,textarea', true);
        var shouldIgnoreFirstNewline = function (tag, html) { return tag && isIgnoreNewlineTag(tag) && html[0] === '\n'; };

        function decodeAttr (value, shouldDecodeNewlines) {
            var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;
            return value.replace(re, function (match) { return decodingMap[match]; })
        }

        function makeMap(
            str,
            expectsLowerCase
        ) {
            var map = Object.create(null);
            var list = str.split(',');
            for (var i = 0; i < list.length; i++) {
                map[list[i]] = true;
            }
            return expectsLowerCase
                ? function (val) { return map[val.toLowerCase()]; }
                : function (val) { return map[val]; }
        }

        parseHTML('<div id="app">Hello, <b>Vue!</b></div>', {})
        /*
        解析HTML字符串的，它将HTML字符串解析为一系列的标签、文本、注释等
        并通过调用options中的回调函数来构建AST（抽象语法树）。
        */
        function parseHTML(html, options) {
            //1. 初始化变量
            //用于存储未闭合的标签信息（栈结构）。
            var stack = [];
            //表示是否期望HTML解析（通常为true）。
            var expectHTML = options.expectHTML;
            //判断是否是自闭合标签（如`<img>`）的函数，如果不是则使用`no`（一个返回false的函数）。
            var isUnaryTag$$1 = options.isUnaryTag || no;
            //判断哪些标签可以省略结束标签（如`<p>`可以不用`</p>`，浏览器会自动闭合）。
            var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
            //记录当前解析到字符串的位置。
            var index = 0;
            //`last`：用于在循环中记录上一次的html字符串，以便检查是否没有进展（陷入死循环）。
            //`lastTag`：记录上一次解析的标签名。
            var last, lastTag;

            //2. 主循环（while (html)）
            //只要html字符串不为空，就一直循环解析。
            while (html) {
                last = html;
                //3. 非纯文本元素处理
                // Make sure we're not in a plaintext content element like script/style
                //`isPlainTextElement`：判断当前标签是否是纯文本元素（即标签内的内容应作为纯文本处理，不解析标签）。
                if (!lastTag || !isPlainTextElement(lastTag)) {
                    // 4. 处理非纯文本元素
                    //在非纯文本元素内，我们尝试解析各种标签类型。

                    // 4.1 查找第一个`<`的位置
                    var textEnd = html.indexOf('<');
                    if (textEnd === 0) {
                        //说明当前以标签开始（可能是开始标签、结束标签、注释等）。


                        //4.5 处理结束标签
                        // End tag:
                        var endTagMatch = html.match(endTag);
                        if (endTagMatch) {
                            //使用正则匹配结束标签（如`</div>`），然后调用`parseEndTag`处理。
                            var curIndex = index;
                            advance(endTagMatch[0].length);
                            parseEndTag(endTagMatch[1], curIndex, index);
                            continue
                        }

                        // 4.6 处理开始标签
                        // Start tag:
                        //调用`parseStartTag`尝试匹配开始标签，如果匹配成功，则调用`handleStartTag`处理。
                        var startTagMatch = parseStartTag();
                        if (startTagMatch) {
                            handleStartTag(startTagMatch);
                            // 忽略某些标签后的第一个换行（如pre标签）
                            if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
                                advance(1);
                            }
                            continue
                        }
                    }

                    // 4.7 处理文本 说明当前位置到第一个`<`之间是文本内容。
                    var text = (void 0), rest = (void 0), next = (void 0);
                    if (textEnd >= 0) {
                        // 从textEnd位置开始截取剩余字符串
                        rest = html.slice(textEnd);
                        // 循环确保我们不是在标签内（比如在文本中有<符号）
                        while (
                            !endTag.test(rest) &&
                            !startTagOpen.test(rest) &&
                            !comment.test(rest) &&
                            !conditionalComment.test(rest)
                        ) {
                            // 在纯文本中遇到<，继续往后找
                            // < in plain text, be forgiving and treat it as text
                            next = rest.indexOf('<', 1);
                            if (next < 0) { break }
                            textEnd += next;
                            rest = html.slice(textEnd);
                        }
                        text = html.substring(0, textEnd);
                    }

                    // 如果整个剩余字符串都没有<，则全部是文本
                    if (textEnd < 0) {
                        text = html;
                    }

                    // 跳过文本
                    if (text) {
                        advance(text.length);
                    }

                    // 调用chars回调处理文本
                    if (options.chars && text) {
                        options.chars(text, index - text.length, index);
                    }
                } else {
                    //在纯文本元素中，我们使用正则表达式匹配结束标签，然后将整个纯文本内容提取出来，调用`options.chars`。
                    //5. 处理纯文本元素（如script, style）
                    var endTagLength = 0;
                    var stackedTag = lastTag.toLowerCase();
                    var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
                    var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
                        endTagLength = endTag.length;
                        // 如果不是纯文本元素（这里可能是noscript？）且不是noscript，则移除注释和CDATA
                        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
                            text = text.replace(/<!\--([\s\S]*?)--> /g, '$1').replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
                        }
                        // 忽略第一个换行
                        if (shouldIgnoreFirstNewline(stackedTag, text)) {
                            text = text.slice(1);
                        }
                        if (options.chars) {
                            options.chars(text);
                        }
                        return ''
                    });
                    index += html.length - rest$1.length;
                    html = rest$1;
                    // 解析结束标签
                    parseEndTag(stackedTag, index - endTagLength, index);
                }

                // 如果经过一轮处理html没有变化，说明可能有错误
                if (html === last) {
                    options.chars && options.chars(html);
                    if (!stack.length && options.warn) {
                        options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length });
                    }
                    break
                }
            }

            //7. 清理剩余标签
            //循环结束后，调用`parseEndTag`清理栈中未闭合的标签。
            // Clean up any remaining tags
            parseEndTag();

            //将字符串指针前进n个字符，同时更新index和html字符串。
            function advance(n) {
                index += n;
                html = html.substring(n);
            }

            //解析开始标签，返回一个包含标签名、属性的对象。
            function parseStartTag() {
                var start = html.match(startTagOpen);
                if (start) {
                    var match = {
                        tagName: start[1],
                        attrs: [],
                        start: index
                    };
                    advance(start[0].length);
                    var end, attr;
                    while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
                        attr.start = index;
                        advance(attr[0].length);
                        attr.end = index;
                        match.attrs.push(attr);
                    }
                    if (end) {
                        match.unarySlash = end[1];
                        advance(end[0].length);
                        match.end = index;
                        return match
                    }
                }
            }

            //处理开始标签，包括处理属性、自闭合标签、入栈等。
            function handleStartTag(match) {
                var tagName = match.tagName;
                var unarySlash = match.unarySlash;

                if (expectHTML) {
                    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
                        parseEndTag(lastTag);
                    }
                    if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
                        parseEndTag(tagName);
                    }
                }

                var unary = isUnaryTag$$1(tagName) || !!unarySlash;

                var l = match.attrs.length;
                var attrs = new Array(l);
                for (var i = 0; i < l; i++) {
                    var args = match.attrs[i];
                    var value = args[3] || args[4] || args[5] || '';
                    var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
                        ? options.shouldDecodeNewlinesForHref
                        : options.shouldDecodeNewlines;
                    attrs[i] = {
                        name: args[1],
                        value: decodeAttr(value, shouldDecodeNewlines)
                    };
                    if (options.outputSourceRange) {
                        attrs[i].start = args.start + args[0].match(/^\s*/).length;
                        attrs[i].end = args.end;
                    }
                }

                if (!unary) {
                    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end });
                    lastTag = tagName;
                }

                if (options.start) {
                    options.start(tagName, attrs, unary, match.start, match.end);
                }
            }

            //处理结束标签，从栈中弹出对应的开始标签，并调用`options.end`回调。
            function parseEndTag(tagName, start, end) {
                var pos, lowerCasedTagName;
                if (start == null) { start = index; }
                if (end == null) { end = index; }

                // Find the closest opened tag of the same type
                if (tagName) {
                    lowerCasedTagName = tagName.toLowerCase();
                    for (pos = stack.length - 1; pos >= 0; pos--) {
                        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
                            break
                        }
                    }
                } else {
                    // If no tag name is provided, clean shop
                    pos = 0;
                }

                if (pos >= 0) {
                    // Close all the open elements, up the stack
                    for (var i = stack.length - 1; i >= pos; i--) {
                        if (i > pos || !tagName &&
                            options.warn
                        ) {
                            options.warn(
                                ("tag <" + (stack[i].tag) + "> has no matching end tag."),
                                { start: stack[i].start, end: stack[i].end }
                            );
                        }
                        if (options.end) {
                            options.end(stack[i].tag, start, end);
                        }
                    }

                    // Remove the open elements from the stack
                    stack.length = pos;
                    lastTag = pos && stack[pos - 1].tag;
                } else if (lowerCasedTagName === 'br') {
                    if (options.start) {
                        options.start(tagName, [], true, start, end);
                    }
                } else if (lowerCasedTagName === 'p') {
                    if (options.start) {
                        options.start(tagName, [], false, start, end);
                    }
                    if (options.end) {
                        options.end(tagName, start, end);
                    }
                }
            }
        }
    </script>
</body>

</html>